﻿#region copyright
//<copyright>
// Copyright(C) 2012 TrackerRealm Corporation
// This file is part of the open source project - Jazz. http://jazz.codeplex.com
// 
// Jazz is open software: you can redistribute it and/or modify it 
// under the terms of the GNU Affero General Public License (AGPL) as published by 
// the Free Software Foundation, version 3 of the License.
// 
// Jazz is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied warranty 
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
//  See the GNU Affero General Public License (AGPL) for more details.
// 
// You should have received a copy of the GNU General Public 
// License along with Jazz.  If not, see <http://www.gnu.org/licenses/>.
//
// REMOVAL OF THIS NOTICE IS VIOLATION OF THE COPYRIGHT. 
//</copyright>
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Collections.Specialized;
using TrackerRealm.Jazz.Common;
using C = TrackerRealm.Jazz.Common.JazzConstants;
using F = TrackerRealm.Jazz.Client.FormatHelper;
using System.Diagnostics;

namespace TrackerRealm.Jazz.Client
{
    /// <summary>
    /// *** Currently not used ***
    /// All Jazz 'object' classes must inherit from this class.
    /// </summary>
    [EmptyObject("jObjectEmpty")]
    public abstract class jObject : IjStorageUnit, INotifyPropertyChanged, INotifyCollectionChanged
    {
        /// <summary>
        /// 
        /// </summary>
        public static readonly jObject[] EmptyArray = new jObject[0];
        #region CreateSet
        /// <summary>
        /// Creates an immutable collection of unique '<see cref="jObject"/>'s.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="jazzObjs"></param>
        /// <returns></returns>
        public static jSet<T> CreateSet<T>(IEnumerable<T> jazzObjs) where T : jObject
        {
            return new jSet<T>(jazzObjs);
        }
        #endregion
        //
        // Initialization
        //
        #region Constructor
        /// <summary>
        /// Constructor - 
        /// Standard initialization.
        /// </summary>
        protected jObject()
        {
            this.id = Guid.NewGuid();
            this.CurrentState = jState.Empty;
            this.PreviousState = jState.Empty;
           
            this.IsRoot = RootAttribute.IsRootValue(this.GetType());
            StatesCollection states = StatesCollection.Create(this.GetType());
            if (states.StartState != jState.Empty) this.EnterState(states.StartState);
        }
        /// <summary>
        /// Constructor.
        /// <para>When this constructor is used this Jazz object is automatically bound (Nexus.Bind) to
        /// the client nexus.</para>
        /// </summary>
        /// <param name="nexus"></param>
        protected jObject(ClientNexusBase nexus)
        :this() // before standard initialization first
        {
            if (nexus != null)
            {
                this.clientNexus = nexus;
                nexus.Bind(this);
                if (!this.Flags.HasFlag(JazzFlagsType.Bound))
                    throw new ApplicationException(
                        "Binding did not work. Probable cause is the use of the incorrect constructor in 'CreateEmpty'.  "+
                        "Use a constructor that does not have the first arguement of type 'ClientNexus'.");
            }
        }
        #endregion
        #region OnInitialBind
        /// <summary>
        /// Executed before this instance is bound to the client nexus for the first time.
        /// <para>When this method is executed the base Jazz object fields will have been set (except the Flag 'Bound' and 'Initialized' bits).</para>
        /// <para>When this method is executed the 'Grant' attributes are not active nor is the ACL active.</para>
        /// </summary>
        protected internal virtual void OnInitialBind()
        {
        }
        #endregion

        #region OnBind
        /// <summary>
        /// Executed before this instance is bound to the client nexus.
        /// <para>This method will be executed everytime this instance is bound to a client nexus.</para>
        /// <para>When this method is executed the 'Grant' attributes are not active.</para>
        /// </summary>
        protected internal virtual void OnBind()
        {
        }
        #endregion
        #region Flags
        [BaseField]
        private JazzFlagsType flags;
        /// <summary>
        /// 
        /// </summary>
        public JazzFlagsType Flags
        {
            get { return this.flags; }
            internal set { this.flags = value; }
        }
        #endregion
        //
        //  States
        //
        #region States
        /// <summary>
        /// Causes 'this' jObject to enter the state as specified by 'stateName'.
        /// </summary>
        /// <param name="stateName">The name of the new current state.</param>
        protected void EnterState(string stateName)
        {
            jState s;
            if (this.Class == null)  // 'jObect' has been created, but has not been bound to nexus
            {
                if (!StatesCollection.Create(this.GetType()).TryFind(stateName, out s))
                    throw new ArgumentException(string.Format("The state named '{0}' does not exist.", stateName));
            }
            else
            {
                if (!this.Class.States.TryFind(stateName, out s))
                    throw new ArgumentException(string.Format("The state named '{0}' does not exist.", stateName));
            }
            this.EnterState(s);
        }
        /// <summary>
        /// Causes 'this' jObject to enter the state as specified by 'stateName'.
        /// </summary>
        /// <param name="entryState">The new current state.</param>
        protected void EnterState(jState entryState)
        {
            this.PreviousState = this.CurrentState;
            this.CurrentState = entryState;
#if DEBUG
            if (this.ClientNexus != null && ((this.ClientNexus.Verbose & VerboseType.EnterStates) == VerboseType.EnterStates))
            {
                StackTrace stackTrace = new StackTrace();

                // get calling method name
                string methodname = stackTrace.GetFrame(1).GetMethod().Name;
                string callType = "Method";
                if(methodname.StartsWith("get_") | methodname.StartsWith("set_"))
                {
                    methodname = methodname.Remove(0, 4);
                    callType = "Property";
                }
                Console.WriteLine(string.Format("{0}Entered the state '{1}' via the {2} '{3}'. The previous state is '{4}'.", 
                    F.Prefix(this, "EnterState"),
                    CurrentState.Name, 
                    callType,
                    methodname,
                    PreviousState.Name));
            }
#endif
            // execute the state method.
            this.CurrentState.Execute(this);
        }
        #region PreviousState
        //[Jazz(AttributeExclude = true)]
        [BaseField]
        private jState previousState;
        /// <summary>
        /// 
        /// </summary>
        public jState PreviousState
        {
            get { return this.previousState; }
            private set { 
                this.previousState = value;
                if(PropertyChanged != null)
                    this.OnPropertyChange(new PropertyChangedEventArgs(C.PREVIOUSSTATE));
            }
        }
        #endregion
        #region CurrentState
        //[Jazz(AttributeExclude = true)]
        [BaseField]
        private jState currentState;
        /// <summary>
        /// 
        /// </summary>
        public jState CurrentState
        {
            get { return this.currentState; }
            private set 
            { 
                this.currentState = value;
                if (PropertyChanged != null)
                    this.OnPropertyChange(new PropertyChangedEventArgs(C.CURRENTSTATE));
            }
        }
        #endregion
        #endregion
        //
        // ACL
        //
        #region ACL
        [BaseField]
        private jAclItem[] aclArray = jAclItem.EmptyArray;
        
        /// <summary>
        /// The ACL (Access Control List) controls how this '<see cref="jObject"/>' instance
        /// can be accessed.
        /// <para>Note: If the ACL sequence is empty then there are no access restrictions.</para>
        /// </summary>
        internal protected virtual IEnumerable<jAclItem> _ACL
        {
            get { return aclArray.Select(a => a); }
            set
            {
                if (this.IsEmpty) throw new InvalidOperationException("An 'empty' jObject must have an empty ACL.");
                jAclItem[] aArray = value.ToArray();
                // lots of checking needs to be done
                if (aArray.Distinct().Count() != aArray.Count())
                    throw new ArgumentException("Elements are not unique.", "value");
                //if (!aArray.FirstOrDefault(v => v.Role is jProfileBase).IsNull)
                //    throw new ArgumentException("Elements items must have a role.  Profiles are currently not supported.", "value");
                aclArray = aArray;
                this.SetAccessibility(this.clientNexus);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        internal bool IsAccessibilitySet
        {
            get { return this.flags.HasFlag(JazzFlagsType.AccessibilitySet); }
            set
            {
                if (value)
                    this.flags = this.flags.Set(JazzFlagsType.AccessibilitySet);
                else
                    this.flags = this.flags.Reset(JazzFlagsType.AccessibilitySet);
            }
        }
        /// <summary>
        /// Sets the accessibility of this 'jObject' based on the workspace
        /// accessibility and the Acl.
        /// </summary>
        /// <param name="nexus"></param>
        internal void SetAccessibility(ClientNexusBase nexus)
        {
            // first set the accessibility of the workspace.
            if (!this.workspace.IsEmpty && !this.workspace.IsAccessibilitySet && (!this.workspace.IsAccessible || this.workspace.Name != C.WORKSPACE_PUBLIC))
                this.workspace.SetAccessibility(nexus);
            // check workspace - adopt accessibility of workspace
            this.IsAccessibilitySet = true;
            if (!this.workspace.IsAccessible)
            {
                this.flags = this.flags.Reset(JazzFlagsType.NotAccessible | JazzFlagsType.ReadOnly);
                nexus.Cache.NamedObjects.TryRemove(this);
                this.flags = this.flags.Set(JazzFlagsType.NotAccessible);
                return;
            }
            if (this.workspace.IsReadOnly)
            {
                this.flags = this.flags.Reset(JazzFlagsType.NotAccessible);
                this.flags = this.flags.Set(JazzFlagsType.ReadOnly);
                if (!nexus.Cache.NamedObjects.Contains(this) && this.Name != null && this.Name != "")
                    nexus.Cache.NamedObjects.Add(this);
                return;
            }
            // no ACL, therefore accessible
            if (this.aclArray == null || this.aclArray.Length == 0)
            {
                this.flags = this.flags.Reset(JazzFlagsType.NotAccessible);
                if (!this.IsEmpty) this.flags = this.flags.Reset(JazzFlagsType.ReadOnly);
                if (!nexus.Cache.NamedObjects.Contains(this) && this.Name != null && this.Name != "")
                    nexus.Cache.NamedObjects.Add(this);
                return;
            }
            // an ACL exists - only 3 possibilities 
            // 1) full access
            // 2) read only access
            // 3) no access
            bool isAccessible = false;
            bool isReadOnly = false;
            foreach (jAclItem item in aclArray)
            {
                if (item.Control == AccessControlType.ReadOnly && (nexus._LoginRoles.Contains(item.Role) || nexus._LoginProfile == item.Role))
                {
                    isReadOnly = true;
                    break;
                }
            }
            foreach (jAclItem item in aclArray)
            {
                if (item.Control == AccessControlType.Accessible && (nexus._LoginRoles.Contains(item.Role) || nexus._LoginProfile == item.Role))
                {
                    // 1) full access - because there is an explicit AclItem
                    this.flags = this.flags.Reset(JazzFlagsType.NotAccessible);
                    if (!IsEmpty) this.flags = this.Flags.Reset(JazzFlagsType.ReadOnly);
                    if (!nexus.Cache.NamedObjects.Contains(this) && this.Name != null && this.Name != "")
                        nexus.Cache.NamedObjects.Add(this);
                    return;
                }
            }
            if (isReadOnly)
            {
                // 2) read only access because there is an explicit AclItem giving ReadOnly access
                //    and no AclItem giving full access was found
                this.flags = this.flags.Reset(JazzFlagsType.NotAccessible);
                this.flags = this.flags.Set(JazzFlagsType.ReadOnly);
                if (!nexus.Cache.NamedObjects.Contains(this) && this.Name != null && this.Name != "")
                    nexus.Cache.NamedObjects.Add(this);
                return;
            }
            // 3) no access becuase no matching AclItem was found.
            if (!isAccessible)
            {
                this.flags = this.flags.Reset(JazzFlagsType.NotAccessible | JazzFlagsType.ReadOnly);
                nexus.Cache.NamedObjects.TryRemove(this);
                this.flags = this.flags.Set(JazzFlagsType.NotAccessible);
                return;
            }
            throw new InvalidOperationException("Unexpected operation");
        }
        #endregion
        #region Add/Remove AclItem
        /// <summary>
        /// Add a '<see cref="jAclItem"/>' to this jObject's ACL.
        /// </summary>
        /// <param name="item"></param>
        protected void _Add(jAclItem item)
        {
            this._ACL = this._ACL.AddToUniqueSequence(item);
        }
        /// <summary>
        /// Remove a '<see cref="jAclItem"/>' from this jObject's ACL.
        /// </summary>
        /// <param name="item"></param>
        protected void _Remove(jAclItem item)
        {
            this._ACL = this._ACL.RemoveFromSequence(item);
        }
        #endregion
        //
        // Name
        //
        #region Name
        /// <summary>
        /// Returns the name of this '<see cref="jObject"/>'.
        /// <para>If the name is non-blank this jObject instance can be retrieved from the cache via a name lookup ('ClientNexus.Cache.NamedObjects').</para>
        /// <para>If your 'jObject' class requires a unique name then consider using '<see cref="jNameUnique"/>' as a base class.</para>
        /// </summary>
        public virtual string Name
        {
            get { return ""; }
            internal protected set { throw new NotSupportedException(); }
        }
        #endregion
        #region IsNameUnique
        /// <summary>
        /// Returns true indicates that name is unique for all instances of this 'Class'.
        /// <para>For example all '<see cref="jRoleBase"/>' instances will have unique names.</para>
        /// <para>If your 'jObject' class requires a unique name then consider using 'jName' as a base class.</para>
        /// </summary>
        public bool IsNameUnique
        {
            get { return this.Class.IsJazzObjectNameUnique; }
        }
        #endregion
        #region FullName
        /// <summary>
        /// Returns a name in the format of 'server name'.'workspace name'.'this name';
        /// </summary>
        public string FullName
        {
            get
            {
                string name;
                if (this.Name == "")
                    name = this.GetType().Name + ":" + this.ID.ToString();
                else
                    name = this.Name;
                return C.DEFAULTSERVER + "." + this.Workspace.Name + "." + name;
            }
        }
        #endregion
        //
        //  other Instance Properties
        //
        #region Class
        /// <summary>
        /// This field is set during the 'Bind' process.
        /// </summary>
        [SerializeIgnore]
#pragma warning disable
        private jClass jazzClass;
#pragma warning enable
        /// <summary>
        /// Returns the Jazz Workflow Class, that provides Jazz workflow type operations and data.
        /// </summary>
        public jClass Class
        {
            get { return this.jazzClass; }
        }
        #endregion
        #region ID
        /// <summary>
        /// Gets the Guid value which is used to store and retrieve objects from the nexus.
        /// <para>IDs are assigned during the 'Bind' process.  Before the initial bind the ID will be empty.</para>
        /// </summary>
        public Guid ID
        {
            get
            {
                return this.id;
            }
        }
        /// <summary>
        /// 
        /// </summary>
        [BaseField]
        private Guid id = Guid.Empty;
        #endregion
        #region IsAccessible
        /// <summary>
        /// Returns true when this instance is accessible.
        /// <para>False may be returned because access is not allowed due to the ACL or the jObject with this.ID
        /// does not exist.</para>
        /// <para>An '<see cref="AccessViolationException"/>' will be generated any access is attempted when
        /// IsAccessible returns false.</para>
        /// </summary>
        public bool IsAccessible
        {
            get { return !this.flags.HasFlag(JazzFlagsType.NotAccessible); }
        }
        #endregion
        #region IsReadOnly
        /// <summary>
        /// Returns true when this instance is readonly.
        /// <para>When the instance is readonly public properties and fields can be read.  Fields and
        /// properties can not be set.  Methods can not be executed.</para>
        /// <para>An '<see cref="AccessViolationException"/>' will be generated write or execute access is attempted.</para>
        /// </summary>
        public bool IsReadOnly
        {
            // needs to take into account ACL as well.
            get { return this.Flags.HasFlag(JazzFlagsType.ReadOnly); }
            protected set
            {
                if (this.Flags.HasFlag(JazzFlagsType.ReadOnly)) 
                    throw new AccessViolationException();
                if(value)
                    this.flags= this.flags.Set(JazzFlagsType.ReadOnly); 
                else
                    this.flags = this.flags.Reset(JazzFlagsType.ReadOnly); 
            }
        }
        #endregion
        #region IsEmpty
        /// <summary>
        /// Returns true if this is the empty object for this '<see cref="jObject"/>' class.
        /// <para>Empty objects are 'ReadOnly'.</para>
        /// </summary>
        public bool IsEmpty
        {
            get { return this.Flags.HasFlag(JazzFlagsType.Empty); }
            internal set 
            {
                if (value)
                    this.Flags = this.Flags.Set(JazzFlagsType.Empty);
                else
                    this.Flags = this.Flags.Reset(JazzFlagsType.Empty);
            }
        }
        #endregion
        #region IsBound
        /// <summary>
        /// Returns true if this object has been bound ("Bind" operation) to the client nexus.
        /// <para>See http://wiki.jazzfire.net/Bind.ashx </para>
        /// </summary>
        public bool IsBound
        {
            get { return this.Flags.HasFlag(JazzFlagsType.Bound); }
        }
        #endregion
        #region IsRoot

        /// <summary>
        /// Returns true if this object is a 'root' object.
        /// <para>'IsRoot' is an attribute the is only applicable when Jazz objects are persisted.</para>
        /// <para>A Root object, even if it has no references, is not removed by the Server GC unless it is deleted.</para>
        /// </summary>
        public bool IsRoot
        {
            get { return this.Flags.HasFlag(JazzFlagsType.Root); }
            internal set
            {
                if (value)
                    this.Flags = this.Flags.Set(JazzFlagsType.Root);
                else
                    this.Flags = this.Flags.Reset(JazzFlagsType.Root);
            }
           
        }
        #endregion
        #region StorageStatus
        /// <summary>
        /// Changing the name requires that XConst and that 
        /// all ITjNexusStorageUnit be changed as well.
        /// </summary>
        [BaseField]
        private jStorageStatusType storageStatus = jStorageStatusType.Pending;
        /// <summary>
        /// Indicates whether an object's data is pending or confirmed.
        /// 'Pending' would indicate that an object has been changed but the server
        /// nexus has not yet 'Confirmed' the operation.
        /// </summary>
        public jStorageStatusType StorageStatus
        {
            get { return this.storageStatus; }
        }
        #endregion
        #region TimeStamp
        /// <summary>
        /// 
        /// </summary>
        [BaseField]
        private jTimestamp timeStamp = jTimestamp.Empty;
        /// <summary>
        /// Returns the timeStamp that was issued by server nexus.
        /// When blank the object has been created but not written for the first time.
        /// The time stamp is only valid if '<see cref="StorageStatus"/>' is confirmed.
        /// Time stamps are unique within a single server nexus.
        /// </summary>
        public jTimestamp Timestamp
        {
            get { return this.timeStamp; }
        }

        #endregion
        #region Nexus
        /// <summary>
        /// This field is set during the 'Bind' process.
        /// </summary>
        [SerializeIgnore]
        private ClientNexusBase clientNexus = null;
        /// <summary>
        /// Returns the nexus object.
        /// </summary>
        public ClientNexusBase ClientNexus
        {
            get { return this.clientNexus; }
        }
        #endregion
        #region Workspace
        /// <summary>
        /// 
        /// </summary>
        [BaseField]
#pragma warning disable
        private jWorkspaceBase workspace;
#pragma warning enable
        /// <summary>
        /// Returns the 'Workspace' that this <see cref="jObject"/> resides in.
        /// All '<see cref="jObject"/>' instances must reside in 1 workspace and only 1 workspace.
        /// </summary>
        public virtual jWorkspaceBase Workspace
        {
            get { return this.workspace; }
            internal protected set
            {
                this.workspace = value;
                if(this.clientNexus != null)
                   value.SetAccessibility(this.clientNexus);
            }
        }


        #endregion
        #region Status
        [BaseField]
        private jLifeCycleType status = jLifeCycleType.Valid;
        /// <summary>
        /// Indicates whether this jObject instance 'Valid', 'Deleted', 'Broken' or 'Disconnected'.
        /// </summary>
        public jLifeCycleType Status
        {
            get { return this.status; }
        }
        /// <summary>
        /// 
        /// </summary>
        public bool IsDeleted
        {
            get { return this.status.HasFlag(jLifeCycleType.Deleted); }
        }
        /// <summary>
        /// 
        /// </summary>
        public bool IsValid
        {
            get { return this.status.HasFlag(jLifeCycleType.Valid); }
        }
        #endregion
        //
        //  other Instance methods
        //
        #region _Children
        /// <summary>
        /// *** not tested  - may change ***
        /// Returns children.
        /// </summary>
        /// <returns></returns>
        protected internal virtual IEnumerable<jObject> _Children()
        {
            return jObject.EmptyArray;
        }
        /// <summary>
        /// *** not tested  - may change ***
        /// </summary>
        /// <returns></returns>
        protected internal IEnumerable<jObject> _Descendants()
        {
            return this._Descendants(this);
        }
        /// <summary>
        /// *** not tested  - may change ***
        /// </summary>
        /// <param name="jazzObject"></param>
        /// <returns></returns>
        private IEnumerable<jObject> _Descendants(jObject jazzObject)
        {
            foreach (jObject j in jazzObject._Children())
            {
                yield return j;
                foreach (jObject jj in j._Descendants(j))  // recursive
                {
                    yield return jj;
                }
            }
        }
        #endregion
        #region OnAssignID
        /// <summary>
        /// Returns the a guid used as this instance's ID.
        /// <para>IDs are assigned during the 'Bind' process.</para>
        /// <para>
        /// Allows the assignment of predefined IDs.  See "jUser.OnAssignID" for an example.</para>
        /// </summary>
        /// <returns></returns>
        protected internal virtual Guid OnAssignID()
        {
            if (this.id == Guid.Empty)
                return Guid.NewGuid();
            return this.id;
        }
        #endregion
        #region Delete
        /// <summary>
        /// Deletes the workflow.
        /// </summary>
        protected void _Delete()
        {
            this.status = this.status.Set(jLifeCycleType.Deleted);
            this.status = this.status.Reset(jLifeCycleType.Valid);
        }
        #endregion
        #region IjStorageUnit Members
        /// <summary>
        /// 
        /// </summary>
        public jStorageType StorageType
        {
            get { return jStorageType.JazzObject; }
        }
        #endregion
        #region INotifyPropertyChanged Members
        /// <summary>
        /// 
        /// </summary>
        [field:SerializeIgnore]
        public event PropertyChangedEventHandler PropertyChanged;
        /// <summary>
        /// 
        /// </summary>
        /// <param name="e"></param>
        internal void OnPropertyChange(PropertyChangedEventArgs e)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, e);
        }
        #endregion

        #region ToString
        /// <summary>
        /// Designed to be useful for Visual Studio when hovering over variables.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            string name;
            if (this.Name != null && this.Name != "" && this.IsAccessible)
            {
                name = this.Name;
            }
            else
            {
                if (this.Class != null && this.IsEmpty) // bit ugly - will be fixed
                    name = C.EMPTY;
                else
                    name = this.ID.ToString();
            }
                
            return this.GetType().Name + ":'" + name + "'";
        }
        #endregion

        #region INotifyCollectionChanged Members
        /// <summary>
        /// *** Not implemented ***
        /// </summary>
        [field: SerializeIgnore]
        public event NotifyCollectionChangedEventHandler CollectionChanged;
        /// <summary>
        /// 
        /// </summary>
        /// <param name="e"></param>
        internal void OnCollectionChange(NotifyCollectionChangedEventArgs e)
        {
           
            if (this.CollectionChanged != null)
                CollectionChanged(this, e);
        }

        #endregion
        
    }
    /// <summary>
    /// Singleton - Empty for <see cref="jObject"/>
    /// </summary>
    [Jazz]
    [EmptySingleton]
    public class jObjectEmpty : jName
    {
        /// <summary>
        /// 
        /// </summary>
        internal jObjectEmpty()
        :base(C.EMPTY)
        {
        }
    }
}
